using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NUnit.Framework;

namespace TestCases.Assertions
{
    public static class AssertionUtils
    {
        
        public const string DefaultMessage = "Assertion failed. ";
                
        public static void Assert(this IAssertion assertion, bool expression)
        {
            if (!expression)
            {
                assertion.Fail(DefaultMessage);
            }
        }

        public static void Assert(this IAssertion assertion, bool expression, string message)
        {
            if (!expression)
            {
                assertion.Fail(message);
            }
        }
        
        private static IEnumerable<string> SelectPropertiesNames(IEnumerable<PropertyInfo> props)
        {
            return from prop in props
                   where prop.GetIndexParameters().Length == 0
                   select prop.Name;
        }
        
        public static void AssertPropertiesEquals(this IAssertion assertion, object actual, object expected)
        {

            if (ReferenceEquals(actual, expected))
            {
                return;
            }            
            
            if (expected == null)
            {
                assertion.Fail("Actual value is not null, but expected null");
                return;
            }
            
            if (actual == null)
            {
                assertion.Fail("Actual value is null, but expected not null");
                return;
            }
                        
            const BindingFlags defaultFlags = BindingFlags.Public | BindingFlags.Instance;
            
            Type actualType = actual.GetType(), expectedType = expected.GetType();
            PropertyInfo[] actualProperties = actualType.GetProperties(defaultFlags), 
                           expectedProperties = expectedType.GetProperties(defaultFlags);

            var properties = SelectPropertiesNames(actualProperties).Union(SelectPropertiesNames(expectedProperties));
            foreach (var propertyName in properties)
            {
                
                PropertyInfo actualProperty = actualType.GetProperty(propertyName), expectedProperty = expectedType.GetProperty(propertyName);
                if (actualProperty == null)
                {
                    assertion.Fail("Actual object doesn't have property \"" + propertyName + "\".");
                    return;
                }
                
                if (expectedProperty == null)
                {
                    assertion.Fail("Actual object have extra property \"" + propertyName + "\".");
                    return;
                }

                object value1 = actualProperty.GetValue(actual, null), value2 = expectedProperty.GetValue(expected, null);
                if (!Equals(value1, value2))
                {
                    assertion.Fail("Property \"" + propertyName + "\" is not equals (<" + value1 + ">, <" + value2 + ">).");
                    return;
                }
                
            }
        }
        
        private static IAssertion DetermineFrameworkByAttribute(ICustomAttributeProvider method)
        {
            foreach (var attr in method.GetCustomAttributes(false))
            {
                if (attr is TestAttribute || attr is TestCaseAttribute)
                {
                    return new NUnitAssertion();
                }
                if (attr is TestMethodAttribute)
                {
                    return new MSTestAssertion();
                }
            }
            return null;
        }


        public static IAssertion CreateAssert()
        {
            StackFrame[] frames = new StackTrace().GetFrames();
            Debug.Assert(frames != null, "StackTrace returns null on GetFrames().");            
            foreach (var frame in frames)
            {
                IAssertion assertion = DetermineFrameworkByAttribute(frame.GetMethod());
                if (assertion != null)
                {
                    return assertion;
                }
            }
            throw new NotSupportedException("Current test framework is not supported.");
        }
        
        // Note: doesn't support hot switch between different test frameworks
        public static IAssertion Instance
        {
            get { return LazyInitializer.Instance; }
        }

        private static class LazyInitializer
        {
            internal static readonly IAssertion Instance = CreateAssert();

// ReSharper disable EmptyConstructor
            static LazyInitializer()
// ReSharper restore EmptyConstructor
            {
            }
        }
    }
}